2.4 RSS匹配器

最后要看的一部分代码是RSS匹配器的实现代码。我们之前看到的代码搭建了一个框架,以便能够实现不同的匹配器来搜索内容。RSS匹配器的结构与默认匹配器的结构很类似。每个匹配器为了匹配接口, Search 方法的实现都不同,因此匹配器之间无法互相替换。

代码清单2-47中的RSS文档是一个例子。当我们访问数据源列表里RSS数据源的链接时,期望获得的数据就和这个例子类似。

代码清单2-47 期望的RSS数据源文档

<rss xmlns:npr="http://www.npr.org/rss/" xmlns:nprml="http://api"
    <channel>
        <title>News</title>
        <link>...</link>
        <description>...</description>
        <language>en</language>
        <copyright>Copyright 2014 NPR - For Personal Use
        <image>...</image>
        <item>
            <title>
                Putin Says He'll Respect Ukraine Vote But U.S.
            </title>
            <description>
                The White House and State Department have called on the
            </description>

如果用浏览器打开代码清单2-47中的任意一个链接,就能看到期望的RSS文档的完整内容。RSS匹配器的实现会下载这些RSS文档,使用搜索项来搜索标题和描述域,并将结果发送给 results 通道。让我们先看看rss.go代码文件的前12行代码,如代码清单2-48所示。

代码清单2-48 matchers/rss.go:第01行到第12行

01 package matchers
02
03 import (
04     "encoding/xml"
05     "errors"
06     "fmt"
07     "log"
08     "net/http"
09     "regexp"
10
11     "github.com/goinaction/code/chapter2/sample/search"
12 )

和其他代码文件一样,第1行定义了包名。这个代码文件处于名叫 matchers 的文件夹中,所以包名也叫 matchers 。之后,我们从标准库中导入了6个库,还导入了 search 包。再一次,我们看到有些标准库的包是从标准库所在的子文件夹导入的,如 xmlhttp 。就像 json 包一样,路径里最后一个文件夹的名字代表包的名字。

为了让程序可以使用文档里的数据,解码RSS文档的时候需要用到4个结构类型,如代码清单2-49所示。

代码清单2-49 matchers/rss.go:第14行到第58行

14 type (
15     // item根据item字段的标签,将定义的字段
16     // 与rss文档的字段关联起来
17     item struct {
18         XMLName     xml.Name `xml:"item"`
19         PubDate     string   `xml:"pubDate"`
20         Title       string   `xml:"title"`
21         Description string   `xml:"description"`
22         Link        string   `xml:"link"`
23         GUID        string   `xml:"guid"`
24         GeoRssPoint string   `xml:"georss:point"`
25     }
26
27     // image根据image字段的标签,将定义的字段
28     // 与rss文档的字段关联起来
29     image struct {
30         XMLName xml.Name `xml:"image"`
31         URL     string   `xml:"url"`
32         Title   string   `xml:"title"`
33         Link    string   `xml:"link"`
34     }
35
36     // channel根据channel字段的标签,将定义的字段
37     // 与rss文档的字段关联起来
38     channel struct {
39         XMLName        xml.Name `xml:"channel"`
40         Title          string   `xml:"title"`
41         Description    string   `xml:"description"`
42         Link           string   `xml:"link"`
43         PubDate        string   `xml:"pubDate"`
44         LastBuildDate   string   `xml:"lastBuildDate"`
45         TTL            string   `xml:"ttl"`
46         Language       string   `xml:"language"`
47         ManagingEditor string   `xml:"managingEditor"`
48         WebMaster      string   `xml:"webMaster"`
49         Image          image    `xml:"image"`
50         Item           []item   `xml:"item"`
51     }
52
53     // rssDocument定义了与rss文档关联的字段
54     rssDocument struct {
55         XMLName xml.Name `xml:"rss"`
56         Channel channel  `xml:"channel"`
57     }
58 )

如果把这些结构与任意一个数据源的RSS文档对比,就能发现它们的对应关系。解码XML的方法与我们在feed.go代码文件里解码JSON文档一样。接下来我们可以看看 rssMatcher 类型的声明,如代码清单2-50所示。

代码清单2-50 matchers/rss.go:第60行到第61行

60 // rssMatcher 实现了Matcher接口
61 type rssMatcher struct{}

再说明一次,这个声明与 defaultMatcher 类型的声明很像。因为不需要维护任何状态,所以我们使用了一个空结构来实现 Matcher 接口。接下来看看匹配器 init 函数的实现,如代码清单2-51所示。

代码清单2-51 matchers/rss.go:第63行到第67行

63 // init 将匹配器注册到程序里
64 func init() {
65     var matcher rssMatcher
66     search.Register("rss", matcher)
67 }

就像在默认匹配器里看到的一样, init 函数将 rssMatcher 类型的值注册到程序里,以备后用。让我们再看一次main.go代码文件里的导入部分,如代码清单2-52所示。

代码清单2-52 main.go:第07行到第08行

07     _ "github.com/goinaction/code/chapter2/sample/matchers"
08      "github.com/goinaction/code/chapter2/sample/search"

main.go代码文件里的代码并没有直接使用任何 matchers 包里的标识符。不过,我们依旧需要编译器安排调用rss.go代码文件里的 init 函数。在第07行,我们使用下划线标识符作为别名导入 matchers 包,完成了这个调用。这种方法可以让编译器在导入未被引用的包时不报错,而且依旧会定位到包内的 init 函数。我们已经看过了所有的导入、类型和初始化函数,现在来看看最后两个用于实现 Matcher 接口的方法,如代码清单2-53所示。

代码清单2-53 matchers/rss.go:第114行到第140行

114 // retrieve发送HTTP Get请求获取rss数据源并解码
115 func (m rssMatcher) retrieve(feed *search.Feed) (*rssDocument, error) {
116     if feed.URI == "" {
117         return nil, errors.New("No rss feed URI provided")
118     }
119
120     // 从网络获得rss数据源文档
121     resp, err := http.Get(feed.URI)
122     if err != nil {
123         return nil, err
124     }
125
126     // 一旦从函数返回,关闭返回的响应链接
127     defer resp.Body.Close()
128
129     // 检查状态码是不是200,这样就能知道
130     // 是不是收到了正确的响应
131     if resp.StatusCode != 200 {
132         return nil, fmt.Errorf("HTTP Response Error %d\n", resp.StatusCode)
133     }
134
135     // 将rss数据源文档解码到我们定义的结构类型里
136     // 不需要检查错误,调用者会做这件事
137     var document rssDocument
138     err = xml.NewDecoder(resp.Body).Decode(&document)
139     return &document, err
140 }

方法 retrieve 并没有对外暴露,其执行的逻辑是从RSS数据源的链接拉取RSS文档。在第121行,可以看到调用了 http 包的 Get 方法。我们会在第8章进一步介绍这个包,现在只需要知道,使用 http 包,Go语言可以很容易地进行网络请求。当 Get 方法返回后,我们可以得到一个指向 Response 类型值的指针。之后会监测网络请求是否出错,并在第127行安排函数返回时调用 Close 方法。

在第131行,我们检测了 Response 值的 StatusCode 字段,确保收到的响应是 200 。任何不是 200 的请求都需要作为错误处理。如果响应值不是 200 ,我们使用 fmt 包里的 Errorf 函数返回一个自定义的错误。最后3行代码很像之前解码JSON数据文件的代码。只是这次使用 xml 包并调用了同样叫作 NewDecoder 的函数。这个函数会返回一个指向 Decoder 值的指针。之后调用这个指针的 Decode 方法,传入 rssDocument 类型的局部变量 document 的地址。最后返回这个局部变量的地址和 Decode 方法调用返回的错误值。

最后我们来看看实现了 Matcher 接口的方法,如代码清单2-54所示。

代码清单2-54 matchers/rss.go: 第69行到第112行

 69 // Search在文档中查找特定的搜索项
 70 func (m rssMatcher) Search(feed *search.Feed, searchTerm string) 
                                                   ([]*search.Result, error) {
 71     var results []*search.Result
 72
 73     log.Printf("Search Feed Type[%s] Site[%s] For Uri[%s]\n", 
                                               feed.Type, feed.Name, feed.URI)
 74
 75     // 获取要搜索的数据
 76     document, err := m.retrieve(feed)
 77     if err != nil {
 78         return nil, err
 79     }
 80
 81     for _, channelItem := range document.Channel.Item {
 82         // 检查标题部分是否包含搜索项
 83         matched, err := regexp.MatchString(searchTerm, channelItem.Title)
 84         if err != nil {
 85             return nil, err
 86         }
 87
 88         // 如果找到匹配的项,将其作为结果保存
 89         if matched {
 90             results = append(results, &search.Result{
 91                 Field:   "Title",
 92                 Content: channelItem.Title,
 93             })
 94         }
 95
 96         // 检查描述部分是否包含搜索项
 97         matched, err = regexp.MatchString(searchTerm, channelItem.Description)
 98         if err != nil {
 99             return nil, err
100         }
101
102         // 如果找到匹配的项,将其作为结果保存
103         if matched {
104             results = append(results, &search.Result{
105                 Field:   "Description",
106                 Content: channelItem.Description,
107             })
108         }
109     }
110
111     return results, nil
112 }

我们从第71行 results 变量的声明开始分析,如代码清单2-55所示。这个变量用于保存并返回找到的结果。

代码清单2-55 matchers/rss.go:第71行

71     var results []*search.Result

我们使用关键字 var 声明了一个值为 nil 的切片,切片每一项都是指向 Result 类型值的指针。 Result 类型的声明在之前match.go代码文件的第08行中可以找到。之后在第76行,我们使用刚刚看过的 retrieve 方法进行网络调用,如代码清单2-56所示。

代码清单2-56 matchers/rss.go:第75行到第79行

75     // 获取要搜索的数据
76     document, err := m.retrieve(feed)
77     if err != nil {
78         return nil, err
79     }

调用 retrieve 方法返回了一个指向 rssDocument 类型值的指针以及一个错误值。之后,像已经多次看过的代码一样,检查错误值,如果真的是一个错误,直接返回。如果没有错误发生,之后会依次检查得到的RSS文档的每一项的标题和描述,如果与搜索项匹配,就将其作为结果保存,如代码清单2-57所示。

代码清单2-57 matchers/rss.go:第81行到第86行

81     for _, channelItem := range document.Channel.Item {
82         // 检查标题部分是否包含搜索项
83         matched, err := regexp.MatchString(searchTerm, channelItem.Title)
84         if err != nil {
85             return nil, err
86         }

既然 document.Channel.Item 是一个 item 类型值的切片,我们在第81行对其使用 for range 循环,依次访问其内部的每一项。在第83行,我们使用 regexp 包里的 MatchString 函数,对 channelItem 值里的 Title 字段进行搜索,查找是否有匹配的搜索项。之后在第84行检查错误。如果没有错误,就会在第89行到第94行检查匹配的结果,如代码清单2-58所示。

代码清单2-58 matchers/rss.go:第88行到第94行

88         // 如果找到匹配的项,将其作为结果保存
89         if matched {
90             results = append(results, &search.Result{
91                 Field:   "Title",
92                 Content: channelItem.Title,
93             })
94         }

如果调用 MatchString 方法返回的 matched 的值为真,我们使用内置的 append 函数,将搜索结果加入到 results 切片里。 append 这个内置函数会根据切片需要,决定是否要增加切片的长度和容量。我们会在第4章了解关于内置函数 append 的更多知识。这个函数的第一个参数是希望追加到的切片,第二个参数是要追加的值。在这个例子里,追加到切片的值是一个指向 Result 类型值的指针。这个值直接使用字面声明的方式,初始化为 Result 类型的值。之后使用取地址运算符( & ),获得这个新值的地址。最终将这个指针存入了切片。

在检查标题是否匹配后,第97行到第108行使用同样的逻辑检查 Description 字段。最后,在第111行, Search 方法返回了 results 作为函数调用的结果。

results matching ""

    No results matching ""